/**
 * $Id: mxGraphics2DCanvas.java,v 1.192 2011-01-22 10:40:18 gaudenz Exp $
 * Copyright (c) 2007-2010, Gaudenz Alder, David Benson
 */
package com.mxgraph.canvas;

import com.mxgraph.shape.*;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxUtils;
import com.mxgraph.view.mxCellState;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.util.HashMap;
import java.util.Map;

/**
 * An implementation of a canvas that uses Graphics2D for painting.
 */
public class mxGraphics2DCanvas extends mxBasicCanvas {
    /**
     *
     */
    public static final String TEXT_SHAPE_DEFAULT = "default";

    /**
     *
     */
    public static final String TEXT_SHAPE_HTML = "html";

    /**
     * Specifies the image scaling quality. Default is Image.SCALE_SMOOTH.
     */
    public static int IMAGE_SCALING = Image.SCALE_SMOOTH;

    /**
     * Maps from names to mxIVertexShape instances.
     */
    protected static Map<String, mxIShape> shapes = new HashMap<String, mxIShape>();

    /**
     * Maps from names to mxITextShape instances. There are currently three different
     * hardcoded text shapes available here: default, html and wrapped.
     */
    protected static Map<String, mxITextShape> textShapes = new HashMap<String, mxITextShape>();

    /**
     * Static initializer.
     */
    static {
        putShape(mxConstants.SHAPE_ACTOR, new mxActorShape());
        putShape(mxConstants.SHAPE_ARROW, new mxArrowShape());
        putShape(mxConstants.SHAPE_CLOUD, new mxCloudShape());
        putShape(mxConstants.SHAPE_CONNECTOR, new mxConnectorShape());
        putShape(mxConstants.SHAPE_CYLINDER, new mxCylinderShape());
        putShape(mxConstants.SHAPE_CURVE, new mxCurveShape());
        putShape(mxConstants.SHAPE_DOUBLE_ELLIPSE, new mxDoubleEllipseShape());
        putShape(mxConstants.SHAPE_ELLIPSE, new mxEllipseShape());
        putShape(mxConstants.SHAPE_HEXAGON, new mxHexagonShape());
        putShape(mxConstants.SHAPE_IMAGE, new mxImageShape());
        putShape(mxConstants.SHAPE_LABEL, new mxLabelShape());
        putShape(mxConstants.SHAPE_LINE, new mxLineShape());
        putShape(mxConstants.SHAPE_RECTANGLE, new mxRectangleShape());
        putShape(mxConstants.SHAPE_RHOMBUS, new mxRhombusShape());
        putShape(mxConstants.SHAPE_SWIMLANE, new mxSwimlaneShape());
        putShape(mxConstants.SHAPE_TRIANGLE, new mxTriangleShape());
        putTextShape(TEXT_SHAPE_DEFAULT, new mxDefaultTextShape());
        putTextShape(TEXT_SHAPE_HTML, new mxHtmlTextShape());
    }

    /**
     * Optional renderer pane to be used for HTML label rendering.
     */
    protected CellRendererPane rendererPane;

    /**
     * Global graphics handle to the image.
     */
    protected Graphics2D g;

    /**
     * Constructs a new graphics canvas with an empty graphics object.
     */
    public mxGraphics2DCanvas() {
        this(null);
    }

    /**
     * Constructs a new graphics canvas for the given graphics object.
     */
    public mxGraphics2DCanvas(Graphics2D g) {
        this.g = g;

        // Initializes the cell renderer pane for drawing HTML markup
        try {
            rendererPane = new CellRendererPane();
        }
        catch (Exception e) {
            // ignore
        }
    }

    /**
     *
     */
    public static void putShape(String name, mxIShape shape) {
        shapes.put(name, shape);
    }

    /**
     *
     */
    public mxIShape getShape(Map<String, Object> style) {
        String name = mxUtils.getString(style, mxConstants.STYLE_SHAPE, null);

        return shapes.get(name);
    }

    /**
     *
     */
    public static void putTextShape(String name, mxITextShape shape) {
        textShapes.put(name, shape);
    }

    /**
     *
     */
    public mxITextShape getTextShape(Map<String, Object> style, boolean html) {
        String name;

        if (html) {
            name = TEXT_SHAPE_HTML;
        } else {
            name = TEXT_SHAPE_DEFAULT;
        }

        return textShapes.get(name);
    }

    /**
     *
     */
    public CellRendererPane getRendererPane() {
        return rendererPane;
    }

    /**
     * Returns the graphics object for this canvas.
     */
    public Graphics2D getGraphics() {
        return g;
    }

    /**
     * Sets the graphics object for this canvas.
     */
    public void setGraphics(Graphics2D g) {
        this.g = g;
    }

    /*
      * (non-Javadoc)
      * @see com.mxgraph.canvas.mxICanvas#drawCell()
      */
    public Object drawCell(mxCellState state) {
        Map<String, Object> style = state.getStyle();
        mxIShape shape = getShape(style);

        if (g != null && shape != null) {
            // Creates a temporary graphics instance for drawing this shape
            float opacity = mxUtils.getFloat(style, mxConstants.STYLE_OPACITY,
                    100);
            Graphics2D previousGraphics = g;
            g = createTemporaryGraphics(style, opacity, state);

            // Paints the shape and restores the graphics object
            shape.paintShape(this, state);
            g.dispose();
            g = previousGraphics;
        }

        return shape;
    }

    /*
      * (non-Javadoc)
      * @see com.mxgraph.canvas.mxICanvas#drawLabel()
      */
    public Object drawLabel(String text, mxCellState state, boolean html) {
        Map<String, Object> style = state.getStyle();
        mxITextShape shape = getTextShape(style, html);

        if (g != null && shape != null && drawLabels && text != null
                && text.length() > 0) {
            // Creates a temporary graphics instance for drawing this shape
            float opacity = mxUtils.getFloat(style,
                    mxConstants.STYLE_TEXT_OPACITY, 100);
            Graphics2D previousGraphics = g;
            g = createTemporaryGraphics(style, opacity, null);

            // Draws the label background and border
            Color bg = mxUtils.getColor(style,
                    mxConstants.STYLE_LABEL_BACKGROUNDCOLOR);
            Color border = mxUtils.getColor(style,
                    mxConstants.STYLE_LABEL_BORDERCOLOR);
            paintRectangle(state.getLabelBounds().getRectangle(), bg, border);

            // Paints the label and restores the graphics object
            shape.paintShape(this, text, state, style);
            g.dispose();
            g = previousGraphics;
        }

        return shape;
    }

    /**
     *
     */
    public void drawImage(Rectangle bounds, String imageUrl) {
        drawImage(bounds, imageUrl, PRESERVE_IMAGE_ASPECT, false, false);
    }

    /**
     *
     */
    public void drawImage(Rectangle bounds, String imageUrl,
                          boolean preserveAspect, boolean flipH, boolean flipV) {
        if (imageUrl != null && bounds.getWidth() > 0 && bounds.getHeight() > 0) {
            Image img = loadImage(imageUrl);

            if (img != null) {
                int w, h;
                int x = bounds.x;
                int y = bounds.y;

                if (preserveAspect) {
                    double iw = img.getWidth(null);
                    double ih = img.getHeight(null);
                    double s = Math.min(bounds.width / iw, bounds.height / ih);
                    w = (int) (iw * s);
                    h = (int) (ih * s);
                    x += (bounds.width - w) / 2;
                    y += (bounds.height - h) / 2;
                } else {
                    w = bounds.width;
                    h = bounds.height;
                }

                Image scaledImage = img.getScaledInstance(w, h, IMAGE_SCALING);

                if (scaledImage != null) {
                    AffineTransform af = null;

                    if (flipH || flipV) {
                        af = g.getTransform();
                        int sx = 1;
                        int sy = 1;
                        int dx = 0;
                        int dy = 0;

                        if (flipH) {
                            sx = -1;
                            dx = -w - 2 * x;
                        }

                        if (flipV) {
                            sy = -1;
                            dy = -h - 2 * y;
                        }

                        g.scale(sx, sy);
                        g.translate(dx, dy);
                    }

                    g.drawImage(scaledImage, x, y, null);

                    // Restores the previous transform
                    if (af != null) {
                        g.setTransform(af);
                    }
                }
            }
        }
    }

    /**
     *
     */
    public void paintPolyline(mxPoint[] points, boolean rounded) {
        if (points != null && points.length > 1) {
            mxPoint pt = points[0];
            mxPoint pe = points[points.length - 1];

            double arcSize = mxConstants.LINE_ARCSIZE * scale;

            GeneralPath path = new GeneralPath();
            path.moveTo((float) pt.getX(), (float) pt.getY());

            // Draws the line segments
            for (int i = 1; i < points.length - 1; i++) {
                mxPoint tmp = points[i];
                double dx = pt.getX() - tmp.getX();
                double dy = pt.getY() - tmp.getY();

                if ((rounded && i < points.length - 1) && (dx != 0 || dy != 0)) {
                    // Draws a line from the last point to the current
                    // point with a spacing of size off the current point
                    // into direction of the last point
                    double dist = Math.sqrt(dx * dx + dy * dy);
                    double nx1 = dx * Math.min(arcSize, dist / 2) / dist;
                    double ny1 = dy * Math.min(arcSize, dist / 2) / dist;

                    double x1 = tmp.getX() + nx1;
                    double y1 = tmp.getY() + ny1;
                    path.lineTo((float) x1, (float) y1);

                    // Draws a curve from the last point to the current
                    // point with a spacing of size off the current point
                    // into direction of the next point
                    mxPoint next = points[i + 1];
                    dx = next.getX() - tmp.getX();
                    dy = next.getY() - tmp.getY();

                    dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
                    double nx2 = dx * Math.min(arcSize, dist / 2) / dist;
                    double ny2 = dy * Math.min(arcSize, dist / 2) / dist;

                    double x2 = tmp.getX() + nx2;
                    double y2 = tmp.getY() + ny2;

                    path.quadTo((float) tmp.getX(), (float) tmp.getY(),
                            (float) x2, (float) y2);
                    tmp = new mxPoint(x2, y2);
                } else {
                    path.lineTo((float) tmp.getX(), (float) tmp.getY());
                }

                pt = tmp;
            }

            path.lineTo((float) pe.getX(), (float) pe.getY());
            g.draw(path);
        }
    }

    /**
     *
     */
    public void paintRectangle(Rectangle bounds, Color background, Color border) {
        if (background != null) {
            g.setColor(background);
            fillShape(bounds);
        }

        if (border != null) {
            g.setColor(border);
            g.draw(bounds);
        }
    }

    /**
     *
     */
    public void fillShape(Shape shape) {
        fillShape(shape, false);
    }

    /**
     *
     */
    public void fillShape(Shape shape, boolean shadow) {
        int shadowOffsetX = (shadow) ? mxConstants.SHADOW_OFFSETX : 0;
        int shadowOffsetY = (shadow) ? mxConstants.SHADOW_OFFSETY : 0;

        if (shadow) {
            // Saves the state and configures the graphics object
            Paint p = g.getPaint();
            Color previousColor = g.getColor();
            g.setColor(mxConstants.SHADOW_COLOR);
            g.translate(shadowOffsetX, shadowOffsetY);

            // Paints the shadow
            fillShape(shape, false);

            // Restores the state of the graphics object
            g.translate(-shadowOffsetX, -shadowOffsetY);
            g.setColor(previousColor);
            g.setPaint(p);
        }

        g.fill(shape);
    }

    /**
     *
     */
    public Stroke createStroke(Map<String, Object> style) {
        double width = mxUtils
                .getFloat(style, mxConstants.STYLE_STROKEWIDTH, 1) * scale;
        boolean dashed = mxUtils.isTrue(style, mxConstants.STYLE_DASHED);
        if (dashed) {
            float[] dashPattern = mxUtils.getFloatArray(style,
                    mxConstants.STYLE_DASH_PATTERN,
                    mxConstants.DEFAULT_DASHED_PATTERN);
            float[] scaledDashPattern = new float[dashPattern.length];

            for (int i = 0; i < dashPattern.length; i++) {
                scaledDashPattern[i] = (float) (dashPattern[i] * scale);
            }

            return new BasicStroke((float) width, BasicStroke.CAP_BUTT,
                    BasicStroke.JOIN_MITER, 10.0f, scaledDashPattern, 0.0f);
        } else {
            return new BasicStroke((float) width);
        }
    }

    /**
     *
     */
    public Paint createFillPaint(mxRectangle bounds, Map<String, Object> style) {
        Color fillColor = mxUtils.getColor(style, mxConstants.STYLE_FILLCOLOR);
        Paint fillPaint = null;

        if (fillColor != null) {
            Color gradientColor = mxUtils.getColor(style,
                    mxConstants.STYLE_GRADIENTCOLOR);

            if (gradientColor != null) {
                String gradientDirection = mxUtils.getString(style,
                        mxConstants.STYLE_GRADIENT_DIRECTION);

                float x1 = (float) bounds.getX();
                float y1 = (float) bounds.getY();
                float x2 = (float) bounds.getX();
                float y2 = (float) bounds.getY();

                if (gradientDirection == null
                        || gradientDirection
                        .equals(mxConstants.DIRECTION_SOUTH)) {
                    y2 = (float) (bounds.getY() + bounds.getHeight());
                } else if (gradientDirection.equals(mxConstants.DIRECTION_EAST)) {
                    x2 = (float) (bounds.getX() + bounds.getWidth());
                } else if (gradientDirection.equals(mxConstants.DIRECTION_NORTH)) {
                    y1 = (float) (bounds.getY() + bounds.getHeight());
                } else if (gradientDirection.equals(mxConstants.DIRECTION_WEST)) {
                    x1 = (float) (bounds.getX() + bounds.getWidth());
                }

                fillPaint = new GradientPaint(x1, y1, fillColor, x2, y2,
                        gradientColor, true);
            }
        }

        return fillPaint;
    }

    /**
     *
     */
    public Graphics2D createTemporaryGraphics(Map<String, Object> style,
                                              float opacity, mxRectangle bounds) {
        Graphics2D temporaryGraphics = (Graphics2D) g.create();

        // Applies the default translate
        temporaryGraphics.translate(translate.x, translate.y);

        // Applies the rotation on the graphics object
        if (bounds != null) {
            double rotation = mxUtils.getDouble(style,
                    mxConstants.STYLE_ROTATION, 0);

            if (rotation != 0) {
                temporaryGraphics.rotate(Math.toRadians(rotation),
                        bounds.getCenterX(), bounds.getCenterY());
            }
        }

        // Applies the opacity to the graphics object
        if (opacity != 100) {
            temporaryGraphics.setComposite(AlphaComposite.getInstance(
                    AlphaComposite.SRC_OVER, opacity / 100));
        }

        return temporaryGraphics;
    }

}
